Amazon Interactive Video Service が生成するアーカイブから不要な解像度セグメントを自動削除するためのステートマシンを作ってみた
こんにちは、大前です。
Amazon Interactive Video Service(以下 IVS)には S3 へのアーカイブ機能があり、有効化しておくことでライブストリーミングを行いながらアーカイブを S3 に保存することができます。
参考 : Amazon S3 への自動録画 - Amazon Interactive Video Service
上記機能によって生成されるファイルは HLS コンテンツとなっており、下記画像の様にマニフェストファイル(master.m3u8 等)や複数レンディションのコンテンツ(720p60, 360p30 等)が含まれます。
IVS のアーカイブ出力先を利用して録画ファイルの配信などを行うのであれば、複数レンディションが生成されているのはありがたいのですが、単純にアーカイブファイルとして保持しておきたいだけなのであれば、一番大きい解像度のみ(例えば 1080p)を残して他のレンディションは削除したいケースもあるかもしれません。
今回は、Step Functions を利用し、IVS が生成するアーカイブファイルから不要なレンディションを自動で削除する様な仕組みを作ってみました。
構成
IVS はアーカイブ記録の開始時と終了時にイベントを発行するため、それを利用して Step Functions のステートマシンを発火させ、S3 に生成されているアーカイブファイルから特定のレンディションを削除する構成としました。
参考 : 例: 録画状態の変化 - Amazon Interactive Video Service
やってみた
IVS は現状東京リージョンに対応していないため、以下作業は全てバージニア北部(us-east-1)にて行いました。
Step Functions ステートマシンの作成
後ほど細かい部分の説明をしますが、全体像としては以下のステートマシンを作成しました。今回は 160p30
のレンディションを削除する様な設定としています。流用する際は、IVS のアーカイブ保存先の S3 バケット名を記載してください。
{ "Comment": "This is your state machine", "StartAt": "ListObjectsV2", "States": { "ListObjectsV2": { "Type": "Task", "Next": "Map", "Parameters": { "Bucket": "<IVS アーカイブ保存先の S3 バケット名>", "Prefix.$": "States.Format('{}/media/hls/160p30', $.recording_s3_key_prefix)", "MaxKeys": 100 }, "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2", "InputPath": "$.detail", "ResultPath": "$.listresult" }, "Map": { "Type": "Map", "Iterator": { "StartAt": "Pass", "States": { "Pass": { "Type": "Pass", "End": true, "Parameters": { "Key.$": "$.Key" } } } }, "ItemsPath": "$.listresult.Contents", "Next": "DeleteObjects", "ResultPath": "$.mapresult" }, "DeleteObjects": { "Type": "Task", "Parameters": { "Bucket": "<IVS アーカイブ保存先の S3 バケット名>", "Delete": { "Objects.$": "$.mapresult" } }, "Resource": "arn:aws:states:::aws-sdk:s3:deleteObjects", "ResultPath": "$.deleteresult", "Next": "Choice" }, "Choice": { "Type": "Choice", "Choices": [ { "Variable": "$.listresult.IsTruncated", "BooleanEquals": true, "Next": "ListObjectsV2" } ], "Default": "Finish" }, "Finish": { "Type": "Pass", "End": true, "InputPath": "$.deleteresult" } } }
Workflow Studio 上の図
削除対象のオブジェクトの S3 Prefix 一覧を取得
ListObjectsV2
を利用し、削除対象とするオブジェクトの S3 Prefix 一覧を取得しています。
{ "Bucket": "<IVS アーカイブ保存先の S3 バケット名>", "Prefix.$": "States.Format('{}/media/hls/160p30', $.recording_s3_key_prefix)", "MaxKeys": 100 }
API に渡しているパラメータは Bucket/Prefix/MaxKeys
の 3つです。
EventBridge から渡されるイベントからアーカイブが生成されるパス(recording_s3_key_prefix)を取得し、Step Functions の組み込み関数を利用して文字列を結合することで、削除したいレンディションのパスを Prefix として指定しています。
参考 : 組み込み関数 - AWS Step Functions
また、ListObjectsV2 はデフォルトで 1000個のオブジェクトを取得できますが、後述するループ部分の検証のために MaxKeys
を指定して一度に取得するオブジェクト数を指定しています。MaxKeys を 1000 とした方がステートマシン上の状態遷移が少なくなるためコスト的なメリットはありそうですが、副作用などが起こらないかは検証の上でご利用ください。
削除 API に渡すパラメータの整形
ListObjectsV2 にて取得したオブジェクトに対して DeleteObjects を実行し不要なレンディションを削除するのですが、List の結果はそのままでは利用できないため、少しパラメータを整形する必要があります。
ListObjectsV2 の結果は Contents
配下に配列として格納されるため、Map
と Pass
を利用して配列から必要な値だけを抽出して DeleteObjects に渡すパラメータを整形します。
オブジェクトの削除を実行
DeleteObjects
を実行し、指定したレンディションのオブジェクトを削除します。
{ "Bucket": "ivs-archive-042108753690", "Delete": { "Objects.$": "$.mapresult" } }
特に特筆する箇所はありませんが、前段の Map で処理した結果が $.mapresult
に格納されているので、それを Objects
パラメータに渡してあげています。
ループ判定
ライブストリームが数時間に及ぶ場合、一度の 取得→削除 処理では全てのオブジェクトを削除できない場合も考えられるため、Choice
を利用してループ判定を行っています。
"Choices": [ { "Variable": "$.listresult.IsTruncated", "BooleanEquals": true, "Next": "ListObjectsV2" } ]
ListObjectsV2
は取得できるオブジェクトが残っている場合に IsTruncated
に true をセットしてレスポンスを返却するため、今回はこれを利用してループ判定を行なっています。true ならまだオブジェクトが残っているとみなして再度 List→Delete を実施し、false ならオブジェクトが無くなったとして処理を完了させます。
参考 : ListObjectsV2 Response Syntax - Amazon Simple Storage Service
EventBridge ルールの作成
IVS のアーカイブ記録が完了したタイミングで Step Functions を自動起動させたいので、以下のイベントパターンを定義した EventBridge ルールを作成し、ターゲットには上記で作成したステートマシンを指定しました。
{ "source": ["aws.ivs"], "detail-type": ["IVS Recording State Change"], "detail": { "recording_status": ["Recording End"] } }
アーカイブが削除されるか確認してみる
Step Functions ステートマシン、EventBridge ルールの用意ができたら準備完了です。アーカイブ記録機能を有効にした IVS で配信を行い、指定したレンディション(今回は 160p30)が削除されるか確認してみます。
IVS の利用やアーカイブ記録の設定方法は今回省略しますが、必要に応じて以下ブログを参照ください。
今回は大体 40~50分 配信を行なってみました。IVS がアーカイブとして出力するセグメント長は 10秒であるため、約 10秒に 1回のペースでアーカイブのセグメントが追加されており、今回は 250超えのファイルが生成されていることが確認できます。
IVS の配信を終了し、Step Functions のステートマシンを確認すると、ステートマシンが無事実行されたことが確認できます。40秒ほどで実行が完了している様です。
アーカイブ出力先の S3 バケットを確認しに行くと、160p30
配下のオブジェクトが全て削除されているためパス毎無くなっていることが確認できます。
おわりに
IVS が出力するアーカイブのうち、不要なレンディションに関するオブジェクトを自動で削除するための Step Functions ステートマシンを作成してみました。個人的には Lambda なしで実現できたため満足しています。IVS のアーカイブ機能を利用する目的によっては有用なケースもあるかと思いますので、参考にしていただけますと幸いです。
注意点として、この仕組みでは生成されるマスターマニフェストファイル(master.m3u8)には何も更新を行っていないため、レンディション削除後に master.m3u8 を利用したコンテンツ再生を試みると予期せぬ挙動が起こる可能性も考えられます。もし特定のレンディションは削除しつつ、master.m3u8 を利用した配信は行いたい場合、マニフェストの修正はお忘れなく。
以上、AWS 事業本部の大前でした。
利用した CloudFormation テンプレート
検証のために作成した CloudFormation テンプレートを置いておきます。ご参考までに。
delete-ivs-archive-sample.yml
AWSTemplateFormatVersion: "2010-09-09" Description: "A template for automation to delete unnecessary ivs archive rendition." Parameters: # 作成リソースに付与する接頭語 Prefix: Description: "Prefix of each resource" Type: "String" Default: "sample" # IVS のアーカイブ保存先バケット名 BucketName: Description: "Target Bucket name to delete ivs archive" Type: "String" # 削除するレンディション DeleteRendition: Description: "Target Rendition to delete ivs archive" Type: "String" Default: "160p30" AllowedValues: - "1080p" - "720p30" - "480p30" - "360p30" - "160p30" Resources: # Step Functions StateMachine StateMachine: Type: "AWS::StepFunctions::StateMachine" Properties: StateMachineName: !Sub "${Prefix}-delete-ivs-archive-statemachine" DefinitionS3Location: Bucket: "<Step Functions ステートマシンの定義ファイル保存先 S3 バケット名>" Key: "delete-ivs-archive-sample.json" DefinitionSubstitutions: DeleteRendition: !Ref DeleteRendition BucketName: !Ref BucketName RoleArn: !GetAtt StateMachineRole.Arn Tags: - Key: "Name" Value: !Sub "${Prefix}-delete-ivs-archive-statemachine" ## IAM Role for StateMachie StateMachineRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${Prefix}-delete-ivs-archive-statemachine-role" Path: "/service-role/" AssumeRolePolicyDocument: Statement: - Effect: "Allow" Principal: Service: - "states.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - !Ref DeleteIVSArchivePolicy ## IAM Policy for DeleteIVSArchive DeleteIVSArchivePolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${Prefix}-delete-ivs-archive-policy" Path: "/service-role/" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "s3:ListBucket" Resource: - !Join - "" - - "arn:aws:s3:::" - !Ref BucketName - Effect: "Allow" Action: - "s3:DeleteObject" Resource: - !Join - "" - - "arn:aws:s3:::" - !Ref BucketName - "/*" # EventRule for CreateManagedAccount EventRule: Type: "AWS::Events::Rule" Properties: Description: "Catch 'Finish IVS Recording' event" EventPattern: |- { "source": ["aws.ivs"], "detail-type": ["IVS Recording State Change"], "detail": { "recording_status": ["Recording End"] } } Name: !Sub "${Prefix}-catch-FinishIVSRecording" State: "ENABLED" Targets: - Arn: !Ref StateMachine Id: !Sub "${Prefix}-delete-ivs-archive-statemachine" RoleArn: !GetAtt EventBridgeRole.Arn ## IAM Role for EventBridge EventBridgeRole: Type: "AWS::IAM::Role" Properties: RoleName: !Sub "${Prefix}-catch-FinishIVSRecording-role" Path: "/service-role/" AssumeRolePolicyDocument: Statement: - Effect: "Allow" Principal: Service: - "events.amazonaws.com" Action: - "sts:AssumeRole" ManagedPolicyArns: - !Ref InvokeStepFunctionsPolicy ## IAM Policy to invoke Step Functions InvokeStepFunctionsPolicy: Type: "AWS::IAM::ManagedPolicy" Properties: ManagedPolicyName: !Sub "${Prefix}-invoke-step-functions-from-eventbridge-policy" Path: "/service-role/" PolicyDocument: Version: "2012-10-17" Statement: - Resource: - !Ref StateMachine Effect: "Allow" Action: - "states:StartExecution"
delete-ivs-archive-sample.json
{ "Comment": "This is your state machine", "StartAt": "ListObjectsV2", "States": { "ListObjectsV2": { "Type": "Task", "Next": "Map", "Parameters": { "Bucket": "${BucketName}", "Prefix.$": "States.Format('{}/media/hls/${DeleteRendition}', $.recording_s3_key_prefix)", "MaxKeys": 100 }, "Resource": "arn:aws:states:::aws-sdk:s3:listObjectsV2", "InputPath": "$.detail", "ResultPath": "$.listresult" }, "Map": { "Type": "Map", "Iterator": { "StartAt": "Pass", "States": { "Pass": { "Type": "Pass", "End": true, "Parameters": { "Key.$": "$.Key" } } } }, "ItemsPath": "$.listresult.Contents", "Next": "DeleteObjects", "ResultPath": "$.mapresult" }, "DeleteObjects": { "Type": "Task", "Parameters": { "Bucket": "${BucketName}", "Delete": { "Objects.$": "$.mapresult" } }, "Resource": "arn:aws:states:::aws-sdk:s3:deleteObjects", "ResultPath": "$.deleteresult", "Next": "Choice" }, "Choice": { "Type": "Choice", "Choices": [ { "Variable": "$.listresult.IsTruncated", "BooleanEquals": true, "Next": "ListObjectsV2" } ], "Default": "Finish" }, "Finish": { "Type": "Pass", "End": true, "InputPath": "$.deleteresult" } } }